/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.serialize;

import java.awt.RenderingHints;
import java.awt.color.ColorSpace;
import java.awt.color.ICC_ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.SampleModel;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.eclipse.imagen.FloatDoubleColorModel;

/**
 * This class is a serializable proxy for a ColorModel from which the ColorModel may be reconstituted.
 *
 * @since 1.1
 */
public class ColorModelState extends SerializableStateImpl {
    /** Flag indicating that the ColorSpace is unknown. */
    private static final int COLORSPACE_OTHERS = 0;

    /**
     * Flag indicating that the ColorSpace is one of those of which an instance may be obtained using
     * ColorSpace.getInstance() with one of the pre-defined constants ColorSpace.CS_*.
     */
    private static final int COLORSPACE_PREDEFINED = 1;

    /** Flag indicating that the ColorSpace is an ICC_ColorSpace. */
    private static final int COLORSPACE_ICC = 2;

    /** Flag indicating that the ColorModel is null. */
    private static final int COLORMODEL_NULL = 0;

    /** Flag indicating that the ColorModel is a FloatDoubleColorModel. */
    private static final int COLORMODEL_FLOAT_DOUBLE_COMPONENT = 1;

    /** Flag indicating that the ColorModel is a ComponentColorModel. */
    private static final int COLORMODEL_COMPONENT = 2;

    /** Flag indicating that the ColorModel is a IndexColorModel. */
    private static final int COLORMODEL_INDEX = 3;

    /** Flag indicating that the ColorModel is a DirectColorModel. */
    private static final int COLORMODEL_DIRECT = 4;

    /** The ColorModel. */
    private transient ColorModel colorModel = null;

    /**
     * Returns an array of length one containing the pre-defined ColorSpace.CS_* colorspace which equals the parameter
     * ColorSpace or null if it does not equal any of the pre-defined ColorSpaces.
     */
    private static int[] getPredefinedColorSpace(ColorSpace cs) {
        // Initialize an array of the pre-defined ColorSpaces.
        int[] colorSpaces = new int[] {
            ColorSpace.CS_CIEXYZ, ColorSpace.CS_GRAY, ColorSpace.CS_LINEAR_RGB, ColorSpace.CS_PYCC, ColorSpace.CS_sRGB
        };

        // Return the pre-defined index if the parameter is one of these.
        for (int i = 0; i < colorSpaces.length; i++) {
            try {
                if (cs.equals(ColorSpace.getInstance(colorSpaces[i]))) {
                    return new int[] {colorSpaces[i]};
                }
            } catch (Throwable e) {
                // profile not found ; resilent.
            }
        }

        // Try to find a similar ColorSpace.
        int numComponents = cs.getNumComponents();
        int type = cs.getType();
        if (numComponents == 1 && type == ColorSpace.TYPE_GRAY) {
            return new int[] {ColorSpace.CS_GRAY};
        } else if (numComponents == 3) {
            if (type == ColorSpace.TYPE_RGB) {
                return new int[] {ColorSpace.CS_sRGB};
            } else if (type == ColorSpace.TYPE_XYZ) {
                return new int[] {ColorSpace.CS_CIEXYZ};
            }
        }

        // Unknown type - too bad!
        return null;
    }

    /** Serialize the parameter ColorSpace object. */
    private static boolean serializeColorSpace(ColorSpace cs, ObjectOutputStream out) throws IOException {
        int[] colorSpaceType = getPredefinedColorSpace(cs);
        boolean isICCColorSpace = (cs instanceof ICC_ColorSpace);

        if (colorSpaceType == null) {
            out.writeInt(COLORSPACE_OTHERS);

            Object object = cs;
            boolean flag = false;
            try {
                Class cls = cs.getClass();
                Method getInstance = cls.getMethod("getInstance", null);
                if (Modifier.isPublic(cls.getModifiers())) {
                    flag = true;
                    object = cls.getName();
                }
            } catch (Exception e) {
            } finally {
                out.writeBoolean(flag);
                out.writeObject(object);
            }
        } else {
            out.writeInt(COLORSPACE_PREDEFINED);
            out.writeInt(colorSpaceType[0]);
        }

        return true;
    }

    /** Derialize the parameter ColorSpace object. */
    private static ColorSpace deserializeColorSpace(ObjectInputStream in) throws IOException, ClassNotFoundException {
        ColorSpace cs = null;
        int colorSpaceType = in.readInt();
        if (colorSpaceType == COLORSPACE_OTHERS) {
            if (in.readBoolean()) {
                String name = (String) in.readObject();
                try {
                    Class cls = Class.forName(name);
                    Method getInstance = cls.getMethod("getInstance", null);
                    cs = (ColorSpace) getInstance.invoke(null, null);

                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                cs = (ColorSpace) in.readObject();
            }
        } else if (colorSpaceType == COLORSPACE_PREDEFINED) {
            cs = ColorSpace.getInstance(in.readInt());
        }

        return cs;
    }

    public static Class[] getSupportedClasses() {
        return new Class[] {
            ComponentColorModel.class,
            FloatDoubleColorModel.class,
            IndexColorModel.class,
            DirectColorModel.class,
            FloatDoubleColorModel.class
        };
    }

    /**
     * Constructs a <code>ColorModelState</code> from a <code>ColorModel</code>.
     *
     * @param c The <code>ColorModel</code> to be serialized.
     * @param o The <code>SampleModel</code> to be serialized.
     * @param h The <code>RenderingHints</code> (ignored).
     */
    public ColorModelState(Class c, Object o, RenderingHints h) {
        super(c, o, h);
    }

    /**
     * Serialize the <code>ColorModelState</code>.
     *
     * @param out The <code>ObjectOutputStream</code>.
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        ColorModel colorModel = (ColorModel) theObject;

        // Write serialized form to the stream.
        if (colorModel == null) {
            out.writeInt(COLORMODEL_NULL);
        } else if (colorModel instanceof ComponentColorModel) {
            ComponentColorModel cm = (ComponentColorModel) colorModel;
            int type = COLORMODEL_COMPONENT;
            if (colorModel instanceof FloatDoubleColorModel) {
                type = COLORMODEL_FLOAT_DOUBLE_COMPONENT;
            }
            out.writeInt(type);
            serializeColorSpace(cm.getColorSpace(), out); // ignore return
            if (type == COLORMODEL_COMPONENT) {
                out.writeObject(cm.getComponentSize());
            }
            out.writeBoolean(cm.hasAlpha());
            out.writeBoolean(cm.isAlphaPremultiplied());
            out.writeInt(cm.getTransparency());
            // Create a SampleModel to get the transferType. This is
            // absurd but is the only apparent way to retrieve this value.
            SampleModel sm = cm.createCompatibleSampleModel(1, 1);
            out.writeInt(sm.getTransferType());
        } else if (colorModel instanceof IndexColorModel) {
            IndexColorModel cm = (IndexColorModel) colorModel;
            out.writeInt(COLORMODEL_INDEX);
            int size = cm.getMapSize();
            int[] cmap = new int[size];
            cm.getRGBs(cmap);
            out.writeInt(cm.getPixelSize());
            out.writeInt(size);
            out.writeObject(cmap);
            out.writeBoolean(cm.hasAlpha());
            out.writeInt(cm.getTransparentPixel());
            // Create a SampleModel to get the transferType. This is
            // absurd but is the only apparent way to retrieve this value.
            SampleModel sm = cm.createCompatibleSampleModel(1, 1);
            out.writeInt(sm.getTransferType());
        } else if (colorModel instanceof DirectColorModel) {
            DirectColorModel cm = (DirectColorModel) colorModel;
            out.writeInt(COLORMODEL_DIRECT);
            boolean csSerialized = serializeColorSpace(cm.getColorSpace(), out);
            if (!csSerialized) {
                out.writeBoolean(cm.hasAlpha());
            }
            out.writeInt(cm.getPixelSize());
            out.writeInt(cm.getRedMask());
            out.writeInt(cm.getGreenMask());
            out.writeInt(cm.getBlueMask());
            if (csSerialized || cm.hasAlpha()) {
                out.writeInt(cm.getAlphaMask());
            }
            if (csSerialized) {
                out.writeBoolean(cm.isAlphaPremultiplied());
                // Create a SampleModel to get the transferType. This is
                // absurd but is the only apparent way to retrieve this
                // value.
                SampleModel sm = cm.createCompatibleSampleModel(1, 1);
                out.writeInt(sm.getTransferType());
            }
        } else {
            throw new RuntimeException(JaiI18N.getString("ColorModelState0"));
        }
    }

    /**
     * Deserialize the <code>ColorModelState</code>.
     *
     * @param in The <code>ObjectInputStream</code>.
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        ColorModel colorModel = null;

        // Read serialized form from the stream.
        ColorSpace cs = null;

        // Switch on first int which is a flag indicating the class.
        switch ((int) in.readInt()) {
            case COLORMODEL_NULL:
                colorModel = null;
                break;
            case COLORMODEL_FLOAT_DOUBLE_COMPONENT:
                if ((cs = deserializeColorSpace(in)) == null) {
                    colorModel = null;
                    return;
                }
                colorModel =
                        new FloatDoubleColorModel(cs, in.readBoolean(), in.readBoolean(), in.readInt(), in.readInt());
                break;
            case COLORMODEL_COMPONENT:
                if ((cs = deserializeColorSpace(in)) == null) {
                    colorModel = null;
                    return;
                }
                colorModel = new ComponentColorModel(
                        cs, (int[]) in.readObject(), in.readBoolean(), in.readBoolean(), in.readInt(), in.readInt());
                break;
            case COLORMODEL_INDEX:
                colorModel = new IndexColorModel(
                        in.readInt(),
                        in.readInt(),
                        (int[]) in.readObject(),
                        0,
                        in.readBoolean(),
                        in.readInt(),
                        in.readInt());
                break;
            case COLORMODEL_DIRECT:
                if ((cs = deserializeColorSpace(in)) != null) {
                    colorModel = new DirectColorModel(
                            cs,
                            in.readInt(),
                            in.readInt(),
                            in.readInt(),
                            in.readInt(),
                            in.readInt(),
                            in.readBoolean(),
                            in.readInt());
                } else if (in.readBoolean()) {
                    colorModel =
                            new DirectColorModel(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readInt());
                } else {
                    colorModel = new DirectColorModel(
                            in.readInt(), in.readInt(),
                            in.readInt(), in.readInt());
                }
                break;
            default:
                // NB: Should never get here.
                throw new RuntimeException(JaiI18N.getString("ColorModelState1"));
        }

        theObject = colorModel;
    }
}
